﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace SlamCms.Data
{
    public static class Deserializer
    {
        public static Func<IDataReader, T> GetClassDeserializer<T>(IDataReader reader, Type concreteType = null, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
        {
            if (concreteType == null)
                concreteType = typeof(T);

            var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), concreteType, new[] { typeof(IDataReader) }, true);

            var il = dm.GetILGenerator();
            il.DeclareLocal(typeof(int));
            il.DeclareLocal(concreteType);
            il.Emit(OpCodes.Ldc_I4_0);
            il.Emit(OpCodes.Stloc_0);
            var properties = concreteType
                .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Select(p => new
                {
                    p.Name,
                    Setter = p.DeclaringType == concreteType ? p.GetSetMethod(true) : p.DeclaringType.GetProperty(p.Name).GetSetMethod(true),
                    Type = p.PropertyType
                })
                .Where(info => info.Setter != null)
                .ToList();
            var fields = concreteType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (length == -1)
            {
                length = reader.FieldCount - startBound;
            }

            var names = TypedList(new { FieldName = "", PropertyName = "" });
            for (int i = startBound; i < startBound + length; i++)
            {
                var fieldName = reader.GetName(i);
                var pointPos = fieldName.IndexOf(".");
                var propertyName = fieldName;
                if (pointPos > 0)
                    propertyName = fieldName.Substring(pointPos + 1);
                names.Add(new { FieldName = fieldName, PropertyName = propertyName });
            }

            var setters = (
                            from n in names
                            let prop = properties.FirstOrDefault(p => string.Equals(p.Name, n.PropertyName, StringComparison.InvariantCulture))
                                       ?? properties.FirstOrDefault(p => string.Equals(p.Name, n.PropertyName, StringComparison.InvariantCultureIgnoreCase))
                                       ?? properties.FirstOrDefault(p => string.Equals(p.Name, n.FieldName, StringComparison.InvariantCulture)) // property case sensitive first
                                       ?? properties.FirstOrDefault(p => string.Equals(p.Name, n.FieldName, StringComparison.InvariantCultureIgnoreCase)) // property case insensitive second
                            let field = prop != null ? null : (fields.FirstOrDefault(p => string.Equals(p.Name, n.FieldName, StringComparison.InvariantCulture)) // field case sensitive third
                                ?? fields.FirstOrDefault(p => string.Equals(p.Name, n.FieldName, StringComparison.InvariantCultureIgnoreCase))) // field case insensitive fourth
                            select new { Name = n, Property = prop, Field = field }
                          ).ToList();

            var getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public)
                         .Where(p => p.GetIndexParameters().Any() && p.GetIndexParameters()[0].ParameterType == typeof(int))
                         .Select(p => p.GetGetMethod()).First();

            int index = startBound;

            il.BeginExceptionBlock();
            // stack is empty
            il.Emit(OpCodes.Newobj, concreteType.GetConstructor(Type.EmptyTypes)); // stack is now [target]
            bool first = true;
            var @allDone = il.DefineLabel();
            foreach (var item in setters)
            {
                if (item.Property != null || item.Field != null)
                {
                    il.Emit(OpCodes.Dup); // stack is now [target][target]
                    Label isDbNullLabel = il.DefineLabel();
                    Label finishLabel = il.DefineLabel();

                    il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader]
                    EmitInt32(il, index); // stack is now [target][target][reader][index]
                    il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index]
                    il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index]
                    il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object]

                    il.Emit(OpCodes.Dup); // stack is now [target][target][value][value]
                    il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null]
                    il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object]

                    // unbox nullable enums as the primitive, i.e. byte etc
                    Type memberType = item.Property != null ? item.Property.Type : item.Field.FieldType;
                    var nullUnderlyingType = Nullable.GetUnderlyingType(memberType);
                    var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum ? nullUnderlyingType : memberType;
                    il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
                    if (nullUnderlyingType != null && nullUnderlyingType.IsEnum)
                    {
                        il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType }));
                    }
                    if (item.Property != null)
                    {
                        il.Emit(OpCodes.Callvirt, item.Property.Setter); // stack is now [target]
                    }
                    else
                    {
                        il.Emit(OpCodes.Stfld, item.Field); // stack is now [target]
                    }
                    il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target]

                    il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value]

                    il.Emit(OpCodes.Pop); // stack is now [target][target]
                    il.Emit(OpCodes.Pop); // stack is now [target]

                    if (first && returnNullIfFirstMissing)
                    {
                        il.Emit(OpCodes.Pop);
                        il.Emit(OpCodes.Ldnull); // stack is now [null]
                        il.Emit(OpCodes.Stloc_1);
                        il.Emit(OpCodes.Br, @allDone);
                    }

                    il.MarkLabel(finishLabel);
                }
                first = false;
                index += 1;
            }
            il.Emit(OpCodes.Stloc_1); // stack is empty
            il.MarkLabel(@allDone);
            il.BeginCatchBlock(typeof(Exception)); // stack is Exception
            il.Emit(OpCodes.Ldloc_0); // stack is Exception, index
            il.Emit(OpCodes.Ldarg_0); // stack is Exception, index, reader
            il.EmitCall(OpCodes.Call, typeof(Deserializer).GetMethod("ThrowDataException"), null);
            il.Emit(OpCodes.Ldnull);
            il.Emit(OpCodes.Stloc_1); // to make it verifiable
            il.EndExceptionBlock();

            il.Emit(OpCodes.Ldloc_1); // stack is empty
            il.Emit(OpCodes.Ret);

            return (Func<IDataReader, T>)dm.CreateDelegate(typeof(Func<IDataReader, T>));
        }

        public static void ThrowDataException(Exception ex, int index, IDataReader reader)
        {
            string name = "(n/a)", value = "(n/a)";
            if (reader != null && index >= 0 && index < reader.FieldCount)
            {
                name = reader.GetName(index);
                object val = reader.GetValue(index);
                if (val == null || val is DBNull)
                {
                    value = "<null>";
                }
                else
                {
                    value = Convert.ToString(val) + " - " + Type.GetTypeCode(val.GetType());
                }
            }
            throw new DataException(string.Format("Error parsing column {0} ({1}={2})", index, name, value), ex);
        }

        private static void EmitInt32(ILGenerator il, int value)
        {
            switch (value)
            {
                case -1: il.Emit(OpCodes.Ldc_I4_M1); break;
                case 0: il.Emit(OpCodes.Ldc_I4_0); break;
                case 1: il.Emit(OpCodes.Ldc_I4_1); break;
                case 2: il.Emit(OpCodes.Ldc_I4_2); break;
                case 3: il.Emit(OpCodes.Ldc_I4_3); break;
                case 4: il.Emit(OpCodes.Ldc_I4_4); break;
                case 5: il.Emit(OpCodes.Ldc_I4_5); break;
                case 6: il.Emit(OpCodes.Ldc_I4_6); break;
                case 7: il.Emit(OpCodes.Ldc_I4_7); break;
                case 8: il.Emit(OpCodes.Ldc_I4_8); break;
                default:
                    if (value >= -128 && value <= 127)
                    {
                        il.Emit(OpCodes.Ldc_I4_S, (sbyte)value);
                    }
                    else
                    {
                        il.Emit(OpCodes.Ldc_I4, value);
                    }
                    break;
            }
        }

        private static List<T> TypedList<T>(T instance)
        {
            return new List<T>();
        }
    }
}
